package models.re.realData;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.Query;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.similar.SimilarityQueries;
import org.apache.lucene.util.Version;
import org.hibernate.search.jpa.FullTextEntityManager;

import akka.actor.ActorRef;
import akka.actor.ActorSystem;
import akka.actor.Props;
import akka.actor.UntypedActor;
import akka.actor.UntypedActorFactory;
import akka.pattern.Patterns;
import akka.routing.RoundRobinRouter;
import akka.util.Timeout;

import play.db.jpa.JPA;
import scala.concurrent.Await;
import scala.concurrent.Future;
import scala.concurrent.duration.Duration;

import models.aaa.Asset;
import models.re.IData;
import models.re.IItem;
import models.re.IRank;
import models.re.IRelation;
import models.re.ISimilarity;
import models.re.simulatedData.Item;
import models.re.simulatedData.Relation;
import models.spider.stats.Stopwatch;

public class RealData implements IData {
	
	// Not necessary. Just for dev testing needs
	public static void test() {
		RealData rd = new RealData();
		for(ISimilarity s: rd.Similarity(3L, 10)) System.out.println(s.getValue());
	}
	
	@Override
	public ArrayList<IRelation> findAllInRelation(String type,String id) {
		// TODO Auto-generated method stub
		// Return all assets sharing a certain relation type with a given asset.
		ArrayList<IRelation> res = new ArrayList<IRelation>();
		if(Relation.str2Relation(type)==Relation.Type.BELONG) {
			String sql = "SELECT e FROM Asset e WHERE e.id = ?";
			Query query = JPA.em().createQuery(sql);
			query.setParameter(1, Long.decode(id));
			@SuppressWarnings("unchecked")
			ArrayList<Asset> tmp = new ArrayList<Asset>(query.getResultList());
			for(Asset asset: tmp) {
				for(Asset link: asset.getLinks()) res.add(new VirtualBelong(asset,link));
			}
			//Logger.info("Belongs: "+res.size());
		}
		String sql = "SELECT * FROM AssetRelation WHERE type = ? AND source_id = ?";
        Query query = JPA.em().createNativeQuery(sql);
        query.setParameter(1, type);
        query.setParameter(2, Long.decode(id));
        @SuppressWarnings("unchecked")
        ArrayList<AssetRelation> tmp = new ArrayList<AssetRelation>(query.getResultList());        
        for(IRelation s: tmp) res.add(s);
        //Logger.info("Relations: "+res.size());
        return res;
	}

	@Override
	public ArrayList<IRelation> findAllInRelation(String type) {
		// TODO Auto-generated method stub
		// Return all assets sharing a certain relation type.
		ArrayList<IRelation> res = new ArrayList<IRelation>();
		if(Relation.str2Relation(type)==Relation.Type.BELONG) {
			String sql = "SELECT e FROM Asset e";
			Query query = JPA.em().createQuery(sql);
			@SuppressWarnings("unchecked")
			ArrayList<Asset> tmp = new ArrayList<Asset>(query.getResultList());
			for(Asset asset: tmp) {
				for(Asset link: asset.getLinks()) res.add(new VirtualBelong(asset,link));
			}
			//Logger.info("Belongs: "+res.size());
		}
		String sql = "SELECT e FROM AssetRelation e WHERE e.type = ? ";
        Query query = JPA.em().createQuery(sql);
        query.setParameter(1, Relation.str2Relation(type));
        @SuppressWarnings("unchecked")
        ArrayList<AssetRelation> tmp = new ArrayList<AssetRelation>(query.getResultList());        
        for(IRelation s: tmp) res.add(s);
        //Logger.info("Relations: "+res.size());
        return res;
	}

	@Override
	public ArrayList<IItem> findAllCertainNodes(String type) {
		// TODO Auto-generated method stub
		// Return all asset of a certain type
		String sql = "SELECT e FROM Asset e WHERE e.type = ? ";
        Query query = JPA.em().createQuery(sql);
        query.setParameter(1, Item.str2Item(type));
        @SuppressWarnings("unchecked")
		ArrayList<Asset> tmp = new ArrayList<Asset>(query.getResultList());
        ArrayList<IItem> res = new ArrayList<IItem>();
        for(IItem s: tmp) res.add(s);
        return res;
	}

	@Override
	public void CreateRelation(Long source, Long target, String type) {
		// TODO Auto-generated method stub
		// Create a relation between two items
		AssetRelation rel = new AssetRelation(Asset.find.byId(source),Asset.find.byId(target),type);
		rel.save();
	}

	@Override
	public ArrayList<models.re.ISimilarity> Similarity(Long sourceId, int limit) {
		// TODO Auto-generated method stub
		// Return limit similarities for a certain asset
		String sql = "SELECT * FROM AssetSimilarity WHERE source_id = ? ORDER BY value DESC LIMIT ?";
        Query query = JPA.em().createNativeQuery(sql);
        query.setParameter(1, sourceId);
        query.setParameter(2, limit);
        @SuppressWarnings("unchecked")
        ArrayList<Object[]> tmp = new ArrayList<Object[]>(query.getResultList());
        ArrayList<ISimilarity> res = new ArrayList<ISimilarity>();
        for(Object[] s: tmp) res.add(AssetSimilarity.findById(((BigInteger)s[0]).longValue()));
        return res;
	}

	@Override
	public ArrayList<models.re.IRank> Rank(Long actorId, int limit) {
		// TODO Auto-generated method stub
		// Return limit ranks for a certain asset
		String sql = "SELECT e FROM AssetRank e WHERE e.source = ? LIMIT = ? ORDER BY e.value";
        Query query = JPA.em().createQuery(sql);
        query.setParameter(1, actorId);
        query.setParameter(2, limit);
        @SuppressWarnings("unchecked")
		ArrayList<AssetRank> tmp = new ArrayList<AssetRank>(query.getResultList().subList(0,limit));
        ArrayList<IRank> res = new ArrayList<IRank>();
        for(IRank s: tmp) res.add(s);
        return res;
	}

	@Override
	public void CreateSimilarity(Long source, Long target, Double value) {
		// TODO Auto-generated method stub
		// Create a similarity
		AssetSimilarity sim = new AssetSimilarity(Asset.find.byId(source),Asset.find.byId(target),value);
		sim.save();
	}

	@Override
	public void CReateRank(Long source, Long target, Double value) {
		// TODO Auto-generated method stub
		// Create a Rank
		AssetRank rank = new AssetRank(Asset.find.byId(source),Asset.find.byId(target),value);
		rank.save();
	}

	@Override
	public ArrayList<models.re.ISimilarity> getAllSimilarities() {
		// Return the list of all similarities
		// TODO Auto-generated method stub
		String sql = "SELECT e FROM AssetSimilarity e";
        Query query = JPA.em().createQuery(sql);
        @SuppressWarnings("unchecked")
		ArrayList<AssetSimilarity> tmp = new ArrayList<AssetSimilarity>(query.getResultList());
        ArrayList<ISimilarity> res = new ArrayList<ISimilarity>();
        for(ISimilarity s: tmp) res.add(s);
        return res;
	}
	
	public void computeSimilarities(String strAsset) {
		// Compute the similarities for a given asset
		org.apache.lucene.search.Query q = null;
		BooleanQuery bq = null;
		int i = 0;
		try {
			Asset myAsset = Asset.find.byId(Long.decode(strAsset));
			bq = new BooleanQuery();
			if(myAsset.getName()!=null) {
				q = SimilarityQueries.
						formSimilarQuery(myAsset.getName(), 
								new StandardAnalyzer(Version.LUCENE_36), "name", null);
				q.setBoost(2);
				bq.add(q, BooleanClause.Occur.SHOULD);
			}
			if(myAsset.getDescription()!=null) {
				q = SimilarityQueries.
						formSimilarQuery(myAsset.getDescription(), 
								new StandardAnalyzer(Version.LUCENE_36), "description", null);
				q.setBoost(2);
				bq.add(q, BooleanClause.Occur.SHOULD);
			}
			bq.add(new TermQuery(new Term("id", myAsset.getId().toString())), BooleanClause.Occur.MUST_NOT);
			FullTextEntityManager session = org.hibernate.search.jpa.Search
                    .getFullTextEntityManager(JPA.em());		
			org.hibernate.search.jpa.FullTextQuery fq= session.createFullTextQuery(bq, Asset.class);
			fq.setMaxResults(10);
			@SuppressWarnings("unchecked")
			List<Asset> assets = fq.getResultList();
			
			for(Asset asset: assets) {
				CreateSimilarity(Long.decode(strAsset),asset.getId(),1.0-i*1.0/10.0);
				i++;
			}			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
	}
	
	@Deprecated
	public void computeAllSimilarities() {
		// Sequential computing of the similarities
		AssetSimilarity.purge();
		Stopwatch sims = new Stopwatch("Compute Similarities");
		for(String strAsset: getAllNodesIds()) {
			computeSimilarities(strAsset);
		}
		sims.stop();
        System.out.println("Similarities computed in =" + sims.elapsed());		
	}

		
	public void computeAllSimilarities(final int threads) {
		// Parallel computing of the similarities
		ActorSystem system = ActorSystem.create("ComputeSimilaritiesSystem");
		ActorRef master = system.actorOf(new Props(new UntypedActorFactory() {
				private static final long serialVersionUID = 1L;
				public UntypedActor create() {
					return new Master(threads);
				}
			}), "master");
		Stopwatch sims = new Stopwatch("Compute Similarities");
		Timeout timeout = new Timeout(Duration.create(1, java.util.concurrent.TimeUnit.HOURS));
		Future<Object> future = Patterns.ask(master, new End(),timeout);
		try {
			Await.result(future,timeout.duration());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		sims.stop();
        System.out.println("Similarities computed in =" + sims.elapsed());
	}

	@Override
	public ArrayList<String> getAllNodesIds() {
		// TODO Auto-generated method stub
		// Return all the existing assets ids
		String sql = "SELECT e.id FROM Asset e";
        Query query = JPA.em().createQuery(sql);
        @SuppressWarnings("unchecked")
		ArrayList<Long> tmp = new ArrayList<Long>(query.getResultList());
        ArrayList<String> result = new ArrayList<String>();
        for(Long val:tmp) result.add(Long.toString(val));
        return result;
	}
	
	
	// Master for the parallel similarities computing
	private static class Master extends UntypedActor {
		
		private ActorRef router=null;
		private ActorRef master=null;
		private int count = 0;
		private Boolean end = false;
		private ArrayList<String> nodesIds = null;
		
		public Master(int threads) {
			this.router = this.getContext().actorOf(new Props(Worker.class).withRouter(new RoundRobinRouter(threads)),"workerRouter");
			JPA.withTransaction(new play.libs.F.Callback0() {
                @Override
                public void invoke() throws Throwable {
                	AssetSimilarity.purge();
                }
            });
			JPA.withTransaction(new play.libs.F.Callback0() {
                @Override
                public void invoke() throws Throwable {
                	loadNodesIds();
                }
            });
			for(String strAsset: nodesIds) {
				router.tell(new Add(strAsset),getSelf());
				count++;
			}
		}
		
		private void loadNodesIds() {
			nodesIds=new RealData().getAllNodesIds();
		}
		
		@Override
		public void onReceive(Object message) throws Exception {
			// TODO Auto-generated method stub
			if (message instanceof Add) {
				router.tell(message, getSelf());		
				count++;				
			} 
			else if(message instanceof Computed) {
				count--;
				System.out.println(count);
				if(count==0 && end) master.tell(new End(),getSelf());
			}
			else if(message instanceof End) {
				end = true;
				this.master=getSender();
				if(count==0 && end) master.tell(new End(),getSelf());
			}
			else {
				unhandled(message);
			}
		}
		
	}
	
	// Worker for the parallel similarities computing
	private static class Worker extends UntypedActor {

		@Override
		public void onReceive(final Object message) throws Exception {
			// TODO Auto-generated method stub
			if (message instanceof Add) {
				final Add add = (Add) message;
				JPA.withTransaction(new play.libs.F.Callback0() {
	                @Override
	                public void invoke() throws Throwable {
	                	new RealData().computeSimilarities(add.getAsset()); 
	                }
	            });
				getSender().tell(new Computed(),getSelf());
			}
			else {
				unhandled(message);
			}
		}
		
	}
	
	// Messages for the parallel similarities computing
	private static final class Add {

        protected String assetId = null;        

		Add(final String assetId) {
            this.assetId = assetId;
        }

        public String getAsset() {
            return assetId;
        }
    }
	
	private static class Computed {}	
	private static class End {}

}
